#!/usr/bin/perl
# cybozu2ical: Convert Cybozu Office 6 calendar into iCal format
#
# $Id: cybozu2ical 448 2007-10-16 13:14:17Z ogawa $

use strict;
use lib 'lib';

use Encode qw/decode_utf8 encode/;
use WWW::CybozuOffice6::Calendar;
use Data::ICal;
use Data::ICal::Entry::Event;
use Data::ICal::Entry::TimeZone;
use Data::ICal::Entry::TimeZone::Standard;
use DateTime;
use Pod::Usage;
use Getopt::Long;

our $VERSION = '0.20';

###
### TRICK (stop escaping for 'exdate' property)
###
*Data::ICal::Property::_value_as_string = sub {
    my $self = shift;
    my $key = shift;
    my $value = defined($self->value()) ? $self->value() : '';
    
    unless ($self->vcal10) {
        my $lc_key = lc($key);
        $value =~ s/\\/\\/gs;
        $value =~ s/\Q;/\\;/gs unless ($lc_key eq 'rrule' || $lc_key eq 'exdate');
        $value =~ s/,/\\,/gs unless ($lc_key eq 'rrule' || $lc_key eq 'exdate');
        $value =~ s/\n/\\n/gs;
        $value =~ s/\\N/\\N/gs;
    }

    return $value;

};

###
### Utility subroutines
###
sub to_icaldate {
    my($dt, $is_full_day) = @_;
    $is_full_day ?
	$dt->ymd('') :
	$dt->ymd('') . 'T' . $dt->hms('') . ($dt->time_zone->is_utc ? 'Z' : '');
}

sub encode_string {
    my($enc, $text) = @_;
    if ($enc eq 'ncr') {
	$text =~ s/(\P{ASCII})/sprintf("&#%d;", ord($1))/eg;
    } else {
	$text = encode($enc, $text);
    }
    $text;
}

sub read_yaml {
    my $file = shift;
    my $yaml;
    if (eval('require YAML::Tiny')) {
	($yaml) = YAML::Tiny::LoadFile($file);
    } elsif (eval('require YAML')) {
	($yaml) = YAML::LoadFile($file);
    }
    if ($@) {
	die "Faild to read yaml file: $@";
    }
    $yaml;
}

###
### Main part
###

# Handle command-line options
my %opt = (
    conf			=> 'config.yaml',
    'compat-google-calendar'	=> 0,
);
GetOptions(\%opt,
	   'output=s',
	   'conf=s',
	   'compat-google-calendar',
	   'debug',
	   'input-csv=s',
	   'output-csv=s',
	   'help',
       ) or pod2usage(2);
pod2usage(1) if $opt{help};

# Read configuration file
my $cfg = read_yaml($opt{conf});
my $time_zone = $cfg->{time_zone} || 'Asia/Tokyo';

# Generate a root 'calendar' object
my $vcalendar = Data::ICal->new();
$vcalendar->add_properties(
    prodid   => "-//as-is.net/Cybozu2ICal $VERSION//EN",
    calscale => 'GREGORIAN',
    method   => 'PUBLISH',
    $cfg->{calname} ? ('X-WR-CALNAME' => $cfg->{calname}) : (),
    'X-WR-TIMEZONE' => $time_zone
);

# Obtain Cybozu Office 6 Calendar items
my $cal = WWW::CybozuOffice6::Calendar->new(%$cfg);

if ($opt{'input-csv'}) {
    $cal->read_from_csv_file($opt{'input-csv'})
	or die "Failed to read CSV file: $opt{'input-csv'}";
} else {
    $cal->request()
	or die "Failed to get Cybozu Office 6 Calendar";
}

# Output the calendar CSV for debugging
if ($opt{'output-csv'}) {
    local *FH;
    open FH, ">$opt{'output-csv'}" or die "Failed to write $opt{'output-csv'}";
    print FH "$_\n" for $cal->response;
    close FH;
}

# For each items, generate an 'event entry' and append it to the calendar
for my $item ($cal->get_items()) {
    my $vevent = Data::ICal::Entry::Event->new();
    my %args = (
	summary     => decode_utf8($item->summary),
	description => decode_utf8($item->description),
	created     => to_icaldate($item->created),
	dtstamp     => to_icaldate($item->modified),
    );

    $args{comment} = decode_utf8($item->comment)
	if $opt{debug} && $item->comment;

    if ($item->is_full_day) {
	$args{dtstart} = [ to_icaldate($item->start, 1), { VALUE => 'DATE' } ];
	$args{dtend}   = [ to_icaldate($item->end,   1), { VALUE => 'DATE' } ];
    } else {
	$args{dtstart} = [ to_icaldate($item->start, 0), { TZID => $time_zone } ];
	$args{dtend}   = [ to_icaldate($item->end,   0), { TZID => $time_zone } ];
    }

    # handle frequency
    if ($item->can('rrule')) {
	# rrule
	my %rrule = %{$item->rrule};
	$rrule{UNTIL} = to_icaldate($rrule{UNTIL}, $item->is_full_day)
	    if $rrule{UNTIL};
	$rrule{WKST} = 'SU'
	    if $opt{'compat-google-calendar'};

	my @rrule_list;
	for (qw(FREQ COUNT INTERVAL BYMONTH BYMONTHDAY WKST BYDAY UNTIL)) {
	    push @rrule_list, $_ . '=' . $rrule{$_}
		if exists $rrule{$_};
	}
	$args{rrule} = join ';', @rrule_list;

	# exdate
	if ($item->exdates) {
	    if ($item->is_full_day) {
		my $exdate = join ',', map { to_icaldate($_, 1) } $item->exdates;
		$args{exdate} = [ $exdate, { VALUE => 'DATE' } ];
	    } else {
		my $exdate = join ',', map { to_icaldate($_, 0) } $item->exdates;
		$args{exdate} = [ $exdate, { TZID => $time_zone } ];
	    }
	}

    }

    # set uid (recommended to be the identical syntax to RFC822)
    $args{uid} = $args{created} . '-' . $item->id . '@' . $cal->host;

    $vevent->add_properties(%args);
    $vcalendar->add_entry($vevent);
}

# Generate a 'timezone entry' and append it to the calendar
my $vtimezone = Data::ICal::Entry::TimeZone->new();
$vtimezone->add_properties(tzid => $time_zone);

# probably we need to support the Daylight Saving Time, but not yet supported.
my $standard = Data::ICal::Entry::TimeZone::Standard->new();
my $std = DateTime->new(year => 1970, month => 1, day => 1,
			hour => 0, minute => 0, second => 0,
			time_zone => $time_zone);
my $offset = DateTime::TimeZone::offset_as_string($std->offset) || '+0900';
my $tzname = $cfg->{tzname} || 'JST';

$standard->add_properties(
    tzoffsetfrom => $offset,
    tzoffsetto   => $offset,
    tzname       => $tzname,
    dtstart      => to_icaldate($std)
);
$vtimezone->add_entry($standard);

$vcalendar->add_entry($vtimezone);

# Outputs the calendar as a string
my $text = encode_string($cfg->{output_encoding} || 'utf8', $vcalendar->as_string);
if ($opt{output}) {
    local *FH;
    open FH, ">$opt{output}" or die "Failed to write $opt{output}";
    print FH $text;
    close FH;
} else {
    print $text;
}

1;
__END__

=head1 NAME

cybozu2ical - Convert Cybozu Office 6 calendar into iCal format

=head1 SYNOPSIS

  % cybozu2ical
  % cybozu2ical --conf /path/to/config.yaml

=head1 DESCRIPTION

C<cybozu2ical> is a command line application that fetches Cybozu
Office 6 calendar items and converts them into a iCal file.  It allows
you to easily integrate the Cybozu Calendar into iCalendar-enabled
Calendar applications, such as Microsoft Outlook, Apple iCal, and of
course, Google Calendar.

You can run this via crontab, for example, every 1 hour.

=head1 REQUIREMENT

This application requires perl 5.8.0 with following Perl modules
installed on your box.

=over 4

=item WWW::CybozuOffice6::Calendar

=item Text::CSV_XS

=item Data::ICal

=item DateTime

=item YAML

=back

=head1 OPTIONS

This application has a command-line option as follows:

=over 4

=item --output /path/to/output.ics

Specifies the output file.  By default, this application outputs to
STDOUT.

=item --conf /path/to/config.yaml

Specifies the configuration file.  By default, C<config.yaml> in the
current directory will be used.

=item --compat-google-calendar

To be compatible with Google Calendar.

=item --debug

Outputs Cybozu Office 6 CSV data as a COMMENT field of each events.
It is just for debugging.

=item --help

Prints out this message.

=back

=head1 CONFIGURATION

The distributions includes a sample configuration file
C<config.yaml.sample>. You can rename it to C<config.yaml> and
configure C<cybozu2ical>.

=over 4

=item cybozu_url

Set the URL of your Cybozu Office 6.

=item username, userid

Set your username or userid for Cybozu Office 6.

=item password

Set your password for Cybozu Office 6.

=item time_zone

Set the timezone of your Cybozu Office 6 (e.g., Asia/Tokyo).

=item tzname

Set the short timezone name of your Cybozu Office 6 (e.g., JST).

=back

=head1 DEVELOPMENT

The development version is always available from the following
subversion repository:

  http://code.as-is.net/svn/public/cybozu2ical/trunk/

You can browse the files via Trac from the following:

  http://code.as-is.net/public/browser/cybozu2ical/trunk/

Any comments, suggestions, or patches are welcome.

=head1 AUTHOR

Hirotaka Ogawa E<lt>hirotaka.ogawa at gmail.comE<gt>

This script is free software and licensed under the same terms as Perl
(Artistic/GPL).

=cut
